"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTransformer = void 0;
const path = require("path");
const ts = require("typescript");
const ts_is_kind_1 = require("./ts-is-kind");
const hash_1 = require("./hash");
const minify_1 = require("./minify");
/** Detects that a node represents a styled function
 * Recognizes the following patterns:
 *
 * styled.tag
 * Component.extend
 * styled(Component)
 * styled('tag')
 * styledFunction.attrs(attributes)
 */
function isStyledFunction(node, identifiers) {
    if (ts_is_kind_1.isPropertyAccessExpression(node)) {
        if (isStyledObject(node.expression, identifiers)) {
            return true;
        }
        if (isStyledExtendIdentifier(node.name.text, identifiers) && isValidComponent(node.expression)) {
            return true;
        }
        return false;
    }
    if (ts_is_kind_1.isCallExpression(node) && node.arguments.length === 1) {
        if (isStyledObject(node.expression, identifiers)) {
            return true;
        }
        if (isStyledAttrs(node.expression, identifiers)) {
            return true;
        }
    }
    return false;
}
function isStyledObjectIdentifier(name, { styled: styledIdentifiers = ['styled'] }) {
    return styledIdentifiers.indexOf(name) >= 0;
}
function isStyledObject(node, identifiers) {
    return node && ts_is_kind_1.isIdentifier(node) && isStyledObjectIdentifier(node.text, identifiers);
}
function isValidComponent(node) {
    return node && ts_is_kind_1.isIdentifier(node) && isValidComponentName(node.text);
}
function isLetter(ch) {
    return ch.toLowerCase() !== ch.toUpperCase();
}
function isValidTagName(name) {
    return isLetter(name[0]) && name[0] === name[0].toLowerCase();
}
function isValidComponentName(name) {
    return isLetter(name[0]) && name[0] === name[0].toUpperCase();
}
function isStyledAttrsIdentifier(name, { attrs: attrsIdentifiers = ['attrs'] }) {
    return attrsIdentifiers.indexOf(name) >= 0;
}
function isStyledAttrs(node, identifiers) {
    return (node &&
        ts_is_kind_1.isPropertyAccessExpression(node) &&
        isStyledAttrsIdentifier(node.name.text, identifiers) &&
        isStyledFunction(node.expression, identifiers));
}
function isStyledKeyframesIdentifier(name, { keyframes = ['keyframes'] }) {
    return keyframes.indexOf(name) >= 0;
}
function isStyledCssIdentifier(name, { css = ['css'] }) {
    return css.indexOf(name) >= 0;
}
function isStyledCreateGlobalStyleIdentifier(name, { createGlobalStyle = ['createGlobalStyle'] }) {
    return createGlobalStyle.indexOf(name) >= 0;
}
function isStyledExtendIdentifier(name, { extend = [] }) {
    return extend.indexOf(name) >= 0;
}
function isMinifyableStyledFunction(node, identifiers) {
    return (isStyledFunction(node, identifiers) ||
        (ts_is_kind_1.isIdentifier(node) &&
            (isStyledKeyframesIdentifier(node.text, identifiers) ||
                isStyledCssIdentifier(node.text, identifiers) ||
                isStyledCreateGlobalStyleIdentifier(node.text, identifiers))));
}
function defaultGetDisplayName(filename, bindingName) {
    return bindingName;
}
function createTransformer({ getDisplayName = defaultGetDisplayName, identifiers = {}, ssr = true, displayName = true, minify = false, componentIdPrefix = '' } = {}) {
    /**
     * Infers display name of a styled component.
     * Recognizes the following patterns:
     *
     * (const|var|let) ComponentName = styled...
     * export default styled...
     */
    function getDisplayNameFromNode(node, sourceFile) {
        if (ts_is_kind_1.isVariableDeclaration(node) && ts_is_kind_1.isIdentifier(node.name)) {
            return (componentIdPrefix ? componentIdPrefix + '-' : '') + getDisplayName(sourceFile.fileName, node.name.text);
        }
        if (ts_is_kind_1.isExportAssignment(node)) {
            return getDisplayName(sourceFile.fileName, undefined);
        }
        return undefined;
    }
    function getIdFromNode(node, sourceRoot, position, sourceFile) {
        if ((ts_is_kind_1.isVariableDeclaration(node) && ts_is_kind_1.isIdentifier(node.name)) || ts_is_kind_1.isExportAssignment(node)) {
            const fileName = sourceFile.fileName;
            const filePath = sourceRoot
                ? path.relative(sourceRoot, fileName).replace(path.sep, path.posix.sep)
                : fileName;
            return (componentIdPrefix !== null && componentIdPrefix !== void 0 ? componentIdPrefix : 'sc') + '-' + hash_1.hash(`${getDisplayNameFromNode(node, sourceFile)}${filePath}${position}`);
        }
        return undefined;
    }
    const transformer = (context) => {
        const { sourceRoot } = context.getCompilerOptions();
        return (sourceFile) => {
            let lastComponentPosition = 0;
            const withConfig = (node, properties) => properties.length > 0
                ? context.factory.createCallExpression(context.factory.createPropertyAccessExpression(node, 'withConfig'), undefined, [context.factory.createObjectLiteralExpression(properties)])
                : node;
            const createDisplayNameConfig = (displayNameValue) => displayNameValue
                ? [
                    context.factory.createPropertyAssignment('displayName', context.factory.createStringLiteral(displayNameValue)),
                ]
                : [];
            const createIdConfig = (componentId) => componentId
                ? [
                    context.factory.createPropertyAssignment('componentId', context.factory.createStringLiteral(componentId)),
                ]
                : [];
            const transformStyledFunction = (binding, node) => withConfig(node, [
                ...(displayName ? createDisplayNameConfig(getDisplayNameFromNode(binding, sourceFile)) : []),
                ...(ssr
                    ? createIdConfig(getIdFromNode(binding, sourceRoot, ++lastComponentPosition, sourceFile))
                    : []),
            ]);
            const transformTemplateLiteral = (templateLiteral) => minify ? minify_1.minifyTemplate(templateLiteral, context.factory) : templateLiteral;
            const transformTaggedTemplateExpression = (node) => isMinifyableStyledFunction(node.tag, identifiers)
                ? context.factory.updateTaggedTemplateExpression(node, node.tag, node.typeArguments, transformTemplateLiteral(node.template))
                : node;
            const transformBindingExpression = (binding, node) => {
                if (ts_is_kind_1.isTaggedTemplateExpression(node) && isStyledFunction(node.tag, identifiers)) {
                    return context.factory.updateTaggedTemplateExpression(node, transformStyledFunction(binding, node.tag), node.typeArguments, transformTemplateLiteral(node.template));
                }
                if (ts_is_kind_1.isCallExpression(node) && isStyledFunction(node.expression, identifiers)) {
                    return context.factory.updateCallExpression(node, transformStyledFunction(binding, node.expression), node.typeArguments, node.arguments);
                }
            };
            const updateNode = (node, data, updateFn) => (data ? updateFn(node, data) : undefined);
            const updateVariableDeclarationInitializer = (node, initializer) => context.factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer);
            const updateExportAssignmentExpression = (node, expression) => context.factory.updateExportAssignment(node, node.decorators, node.modifiers, expression);
            const transformNode = (node) => ts_is_kind_1.isVariableDeclaration(node) && node.initializer
                ? updateNode(node, transformBindingExpression(node, node.initializer), updateVariableDeclarationInitializer)
                : ts_is_kind_1.isExportAssignment(node)
                    ? updateNode(node, transformBindingExpression(node, node.expression), updateExportAssignmentExpression)
                    : minify && ts_is_kind_1.isTaggedTemplateExpression(node)
                        ? transformTaggedTemplateExpression(node)
                        : undefined;
            const visitNode = (node) => transformNode(node) || ts.visitEachChild(node, visitNode, context);
            return ts.visitNode(sourceFile, visitNode);
        };
    };
    return transformer;
}
exports.createTransformer = createTransformer;
exports.default = createTransformer;
